﻿namespace MSSqlSchemaDoc.Provider.MicrosoftSqlServer
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Data.SqlClient;
    using System.Text;
    using MSSqlSchemaDoc.Core;
    using MSSqlSchemaDoc.Core.DataStructures;
    using MSSqlSchemaDoc.Provider.MicrosoftSqlServer.Resources;

    /// <summary>
    /// A concrete implementation of a MS Sql specific scripting manager object.
    /// </summary>
    public partial class SqlScriptingManager : ScriptingManagerBase
    {
        /// <summary>
        /// The database objects to include specifically, in the format "schema_objectname".
        /// </summary>
        private string[] objectIncludes;

        /// <summary>
        /// The database objects to exclude specifically, in the format "schema_objectname".
        /// </summary>
        private string[] objectExcludes;

        /// <summary>
        /// Contains trigger data - if triggers have been included in the scripting options.
        /// The data needs to be saved because it is used both by the trigger creation methods AND the table creation methods.
        /// </summary>
        private DataTable triggerData;

        /// <summary>
        /// A collection of DML triggers that have been scripted.
        /// </summary>
        private List<DmlTrigger> dmlTriggers;

        /// <summary>
        /// The query manager which handles the retrieval of the correct queries.
        /// </summary>
        private QueryManager queryManager;

        /// <summary>
        /// Delegate to run an object scripting task.
        /// </summary>
        /// <param name="connection">A connection.</param>
        /// <param name="objectType">The object type.</param>
        private delegate void ScriptObjectDelegate(SqlConnection connection, DataStructureObjectType objectType);

        /// <summary>
        /// Applies the required filters to the query.
        /// </summary>
        /// <param name="query">The string builder containing the query.</param>
        public void InsertWhereClause(StringBuilder query)
        {
            StringBuilder includes = new StringBuilder();
            StringBuilder excludes = new StringBuilder();

            // Check if we need to specifically include an object
            if (this.objectIncludes != null && this.objectIncludes.Length > 0)
            {
                includes.Append(" ISNULL([schema].[name] + '.', '') + [main].[name] IN ('");

                foreach (string obj in this.objectIncludes)
                {
                    includes.Append(obj);
                    includes.Append("','");
                }

                // Remove the trailing comma and single quote
                includes.Remove(includes.Length - 2, 2);
                includes.Append(")");
            }

            // Check if we need to specifically exclude an object
            if (this.objectExcludes != null && this.objectExcludes.Length > 0)
            {
                excludes.Append(" ISNULL([schema].[name] + '.', '') + [main].[name] NOT IN ('");

                foreach (string obj in this.objectExcludes)
                {
                    excludes.Append(obj);
                    excludes.Append("','");
                }

                // Remove the trailing comma and single quote
                excludes.Remove(includes.Length - 2, 2);
                excludes.Append(")");
            }

            if (includes.Length > 0 || excludes.Length > 0)
            {
                StringBuilder whereClause = new StringBuilder("AND (");

                if (includes.Length > 0)
                {
                    whereClause.Append(includes.ToString());
                }

                if (excludes.Length > 0)
                {
                    if (includes.Length > 0)
                    {
                        whereClause.Append(" AND ");
                    }

                    whereClause.Append(excludes.ToString());
                }

                whereClause.Append(")");
                query.Replace("-- [WHERECLAUSE]", whereClause.ToString());
            }
        }

        /// <summary>
        /// Creates the scripts for the Microsft Sql database.
        /// </summary>
        /// <param name="connectionString">The connection string to the Sql database.</param>
        /// <param name="typesToInclude">The object types to include.</param>
        /// <param name="typesToExclude">The object types to exclude.</param>
        /// <param name="objectsToInclude">The specific objects to include. Use the format "schema_objectname".</param>
        /// <param name="objectsToExclude">The specific objects to exclude. Use the format "schema_objectname"</param>
        protected override void Script(
            string connectionString,
            string[] typesToInclude,
            string[] typesToExclude,
            string[] objectsToInclude,
            string[] objectsToExclude)
        {
            using (SqlConnection connection = new SqlConnection(connectionString))
            {
                connection.Open();
                this.queryManager = new QueryManager(connection);

                this.dmlTriggers = null;
                this.objectIncludes = objectsToInclude;
                this.objectExcludes = objectsToExclude;

                // The order of the following calls may be important for performance reasons
                this.ScriptObject(connection, DataStructureObjectType.DdlTrigger, this.ScriptTriggers);
                this.ScriptObject(connection, DataStructureObjectType.DmlTrigger, this.ScriptTriggers);
                this.ScriptObject(connection, DataStructureObjectType.Table, this.ScriptTables);
                this.ScriptObject(connection, DataStructureObjectType.View, this.ScriptViews);
                this.ScriptObject(connection, DataStructureObjectType.StoredProcedure, this.ScriptStoredProcedures);
                this.ScriptObject(connection, DataStructureObjectType.UserDefinedFunction, this.ScriptUserDefinedFunctions);
                this.ScriptObject(connection, DataStructureObjectType.FullTextCatalog, this.ScriptFullTextCatalogs);
                this.ScriptObject(connection, DataStructureObjectType.Synonym, this.ScriptSynonyms);
            }
        }

        /// <summary>
        /// Override the base implementation so that scripted triggers can be caught and added to a collection,
        /// where it may be re-used by the table scripting methods.
        /// </summary>
        /// <param name="scriptedObject">The object that was just scripted.</param>
        /// <param name="name">The name of the object.</param>
        protected override void OnCompleteScriptingObject(IDataStructureObject scriptedObject, string name)
        {
            // Add the appropriate objects to the table triggers collection
            if (scriptedObject.ObjectType == DataStructureObjectType.DmlTrigger)
            {
                this.dmlTriggers.Add((DmlTrigger)scriptedObject);
            }

            base.OnCompleteScriptingObject(scriptedObject, name);
        }

        /// <summary>
        /// Constructs an object name from a data reader.
        /// </summary>
        /// <param name="reader">The reader containing the data.</param>
        /// <returns>The constructed name.</returns>
        private static string GetObjectName(IDataReader reader)
        {
            return String.Concat(reader["SchemaName"].ToString(), ".", reader["ObjectName"].ToString());
        }

        /// <summary>
        /// Constructs an object name from a data row object.
        /// </summary>
        /// <param name="dataRow">The data row containing the data.</param>
        /// <returns>The constructed name.</returns>
        private static string GetObjectName(DataRow dataRow)
        {
            string schemaName = dataRow.Field<string>("SchemaName");
            string objectName = dataRow.Field<string>("ObjectName");

            if (!String.IsNullOrEmpty(schemaName))
            {
                return String.Concat(schemaName, ".", objectName);
            }
            else
            {
                return objectName;
            }
        }

        /// <summary>
        /// Invokes the required object scripting method while raising the appropriate events.
        /// </summary>
        /// <param name="connection">An open connection.</param>
        /// <param name="objectType">The object type to be scripted.</param>
        /// <param name="scriptMethod">The object scripting method delegate.</param>
        private void ScriptObject(SqlConnection connection, DataStructureObjectType objectType, ScriptObjectDelegate scriptMethod)
        {
            this.OnBeginScriptingNewObjectType(objectType.ToString());
            scriptMethod.Invoke(connection, objectType);
            this.OnCompleteScriptingNewObjectType(objectType.ToString());
        }
    }
}
